package web.suzy.oj.manager.file;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import web.suzy.oj.common.exception.StatusFailException;
import web.suzy.oj.common.exception.StatusSystemErrorException;
import web.suzy.oj.common.result.ResultStatus;
import web.suzy.oj.dao.problem.LanguageEntityService;
import web.suzy.oj.dao.problem.ProblemCaseEntityService;
import web.suzy.oj.dao.problem.ProblemEntityService;
import web.suzy.oj.dao.problem.TagEntityService;
import web.suzy.oj.exception.ProblemIDRepeatException;
import web.suzy.oj.pojo.dto.ProblemDTO;
import web.suzy.oj.pojo.entity.problem.*;
import web.suzy.oj.pojo.vo.ImportProblemVO;
import web.suzy.oj.shiro.AccountProfile;
import web.suzy.oj.utils.Constants;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.*;

/**
 * YangSuzy 软件工作室
 * 类名: ProblemFileManager
 * 描述: ---- 待定 -----
 * 功能: ---- 待定 -----
 * ===========部分代码需要修改============
 *
 * @author YangSuzy
 * Date: 2022/11/30 11:57
 */
@Component
@Slf4j(topic = "suzyoj")
public class ProblemFileManager {

    @Autowired
    private LanguageEntityService languageEntityService;

    @Autowired
    private ProblemEntityService problemEntityService;

    @Autowired
    private TagEntityService tagEntityService;

    @Autowired
    private ProblemCaseEntityService problemCaseEntityService;

    /**
     * 方法名: importProblem
     * 描述: 通过zip文件导入题目
     *
     * @param file zip压缩文件
     * @return void
     * @date 2022/12/2 23:06
     * @auther YangSuzy
     **/
    public void importProblem(MultipartFile file) throws StatusFailException, StatusSystemErrorException {
        //截取文件后缀判断是否为zip压缩文件
        String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".") + 1);
        if (!"zip".toUpperCase().contains(suffix.toUpperCase())) {
            throw new StatusFailException("请上传zip格式的题目压缩文件！");
        }
        //生成文件夹UID
        String fileDirId = IdUtil.simpleUUID();
        //获取默认文件存放路径
        String fileDir = Constants.File.TESTCASE_TMP_FOLDER.getPath() + File.separator + fileDirId;
        //根据文件名和默认路径拼接文件的绝对路径
        String filePath = fileDir + File.separator + file.getOriginalFilename();
        //文件夹不存在时新建文件夹
        FileUtil.mkdir(fileDir);
        try {
            //上传绝对路径
            /**
             * 上线时需要修改
             */
            file.transferTo(new File(filePath).getAbsoluteFile());
        } catch (IOException e) {
            FileUtil.del(fileDir);
            throw new StatusSystemErrorException("服务器异常：评测数据上传失败！");
        }

        // 将压缩包压缩到指定文件夹
        ZipUtil.unzip(filePath, fileDir);
        // 删除zip文件
        FileUtil.del(filePath);
        // 检查文件是否存在
        File testCaseFileList = new File(fileDir);
        File[] files = testCaseFileList.listFiles();
        if (files == null || files.length == 0) {
            FileUtil.del(fileDir);
            throw new StatusFailException("评测数据文件不能为空！");
        }

        //保存题目信息和测试用例信息
        HashMap<String, File> problemInfo = new HashMap<>();
        HashMap<String, File> testcaseInfo = new HashMap<>();

        for (File tmp : files) {
            if (tmp.isFile()) {
                //检查文件是否是json文件
                if (!tmp.getName().endsWith("json")) {
                    FileUtil.del(fileDir);
                    throw new StatusFailException("编号：" + tmp.getName() + "的文件格式错误，请使用json文件！");
                }
                String tmpPreName = tmp.getName().substring(0, tmp.getName().lastIndexOf("."));
                problemInfo.put(tmpPreName, tmp);
            }
            if (tmp.isDirectory()) {
                testcaseInfo.put(tmp.getName(), tmp);
            }
        }

        //读取json文件生成对象
        HashMap<String, ImportProblemVO> problemVoMap = new HashMap<>();
        for (String key : problemInfo.keySet()) {
            //若有名字不对应，直接返回失败
            if (testcaseInfo.getOrDefault(key, null) == null) {
                FileUtil.del(fileDir);
                throw new StatusFailException("请检查编号：" + key + "的题目数据文件与测试数据文件夹是否对应！");
            }
            try {
                FileReader fileReader = new FileReader(problemInfo.get(key));
                ImportProblemVO importProblemVo = JSONUtil.toBean(fileReader.readString(), ImportProblemVO.class);
                problemVoMap.put(key, importProblemVo);
            } catch (Exception e) {
                FileUtil.del(fileDir);
                throw new StatusFailException("请检查编号：" + key + "的题目的json文件格式：" + e.getLocalizedMessage());
            }
        }

        //获取编程语言列表
        List<Language> languageList = languageEntityService.list();
        HashMap<String, Long> languageMap = new HashMap<>();
        for (Language language : languageList) {
            languageMap.put(language.getName(), language.getId());
        }

        //获取当前登录的用户
        AccountProfile userRolesVo = (AccountProfile) SecurityUtils.getSubject().getPrincipal();

        //获取标签列表
        List<ProblemDTO> problemDTOS = new LinkedList<>();
        List<Tag> tagList = tagEntityService.list();
        HashMap<String, Tag> tagMap = new HashMap<>();
        for (Tag tag : tagList) {
            tagMap.put(tag.getName().toUpperCase(), tag);
        }

        //导入题目
        for (String key : problemInfo.keySet()) {
            //获取上传题目视图对象
            ImportProblemVO importProblemVo = problemVoMap.get(key);

            //格式化题目语言
            List<Language> languages = new LinkedList<>();
            for (String lang : importProblemVo.getLanguages()) {
                Long lid = languageMap.getOrDefault(lang, null);
                if (lid == null) {
                    throw new StatusFailException("请检查编号：" + key + "的题目的代码语言，切勿添加不支持的语言！");
                }
                languages.add(new Language().setId(lid).setName(lang));
            }

            //格式化题目代码模板
            List<CodeTemplate> codeTemplates = new LinkedList<>();
            for (Map<String, String> tmp : importProblemVo.getCodeTemplates()) {
                String language = tmp.getOrDefault("language", null);
                String code = tmp.getOrDefault("code", null);
                Long lid = languageMap.getOrDefault(language, null);
                if (language == null || code == null || lid == null) {
                    FileUtil.del(fileDir);
                    throw new StatusFailException("请检查编号：" + key + "的题目的代码模板列表，切勿添加不支持的语言！");
                }
                codeTemplates.add(new CodeTemplate().setCode(code).setStatus(true).setLid(lid));
            }

            // 格式化标签
            List<Tag> tags = new LinkedList<>();
            for (String tagStr : importProblemVo.getTags()) {
                Tag tag = tagMap.getOrDefault(tagStr.toUpperCase(), null);
                if (tag == null) {
                    tags.add(new Tag().setName(tagStr));
                } else {
                    tags.add(tag);
                }
            }

            //设置题目作者
            Problem problem = BeanUtil.mapToBean(importProblemVo.getProblem(), Problem.class, true);
            if (problem.getAuthor() == null) {
                problem.setAuthor(userRolesVo.getUsername());
            }

            //设置测试用例
            List<ProblemCase> problemCaseList = new LinkedList<>();
            for (Map<String, Object> tmp : importProblemVo.getSamples()) {
                problemCaseList.add(BeanUtil.mapToBean(tmp, ProblemCase.class, true));
            }

            //格式化用户额外文件和判题额外文件
            if (importProblemVo.getUserExtraFile() != null) {
                JSONObject userExtraFileJson = JSONUtil.parseObj(importProblemVo.getUserExtraFile());
                problem.setUserExtraFile(userExtraFileJson.toString());
            }
            if (importProblemVo.getJudgeExtraFile() != null) {
                JSONObject judgeExtraFileJson = JSONUtil.parseObj(importProblemVo.getJudgeExtraFile());
                problem.setJudgeExtraFile(judgeExtraFileJson.toString());
            }

            //设置题目信息
            ProblemDTO problemDto = new ProblemDTO();
            problemDto.setProblem(problem)
                    .setCodeTemplates(codeTemplates)
                    .setTags(tags)
                    .setLanguages(languages)
                    .setUploadTestcaseDir(fileDir + File.separator + key)
                    .setIsUploadTestCase(true)
                    .setSamples(problemCaseList);

            //测评模式为空时设置为默认测评模式
            Constants.JudgeMode judgeMode = Constants.JudgeMode.getJudgeMode(importProblemVo.getJudgeMode());
            if (judgeMode == null) {
                problemDto.setJudgeMode(Constants.JudgeMode.DEFAULT.getMode());
            } else {
                problemDto.setJudgeMode(judgeMode.getMode());
            }
            problemDTOS.add(problemDto);
        }

        if (problemDTOS.size() == 0) {
            throw new StatusFailException("警告：未成功导入一道以上的题目，请检查文件格式是否正确！");
        } else {
            //保存重复的题目和导入失败的题目
            HashSet<String> repeatProblemIDSet = new HashSet<>();
            HashSet<String> failedProblemIDSet = new HashSet<>();
            int failedCount = 0;
            for (ProblemDTO problemDto : problemDTOS) {
                try {
                    boolean isOk = problemEntityService.adminAddProblem(problemDto);
                    if (!isOk) {
                        failedCount++;
                    }
                } catch (ProblemIDRepeatException e) {
                    //记录展示ID重复的题目
                    repeatProblemIDSet.add(problemDto.getProblem().getProblemId());
                    failedCount++;
                } catch (Exception e) {
                    log.error("", e);
                    //记录导入失败的题目
                    failedProblemIDSet.add(problemDto.getProblem().getProblemId());
                    failedCount++;
                }
            }

            if (failedCount > 0) {
                int successCount = problemDTOS.size() - failedCount;
                String errMsg = "[导入结果] 成功数：" + successCount + ",  失败数：" + failedCount +
                        ",  重复失败的题目ID：" + repeatProblemIDSet;
                if (failedProblemIDSet.size() > 0) {
                    errMsg = errMsg + "<br/>未知失败的题目ID：" + failedProblemIDSet;
                }
                throw new StatusFailException(errMsg);
            }
        }
    }

    /**
     * 方法名: exportProblem
     * 描述: 导出指定题目和相关测试用例为zip文件
     *
     * @param pidList  题目ID列表
     * @param response 响应
     * @return void
     * @date 2022/12/3 16:30
     * @auther YangSuzy
     **/
    public void exportProblem(List<Long> pidList, HttpServletResponse response) {
        //获取编程语言列表
        List<Language> languageList = languageEntityService.list();
        HashMap<Long, String> languageMap = new HashMap<>();
        for (Language language : languageList) {
            languageMap.put(language.getId(), language.getName());
        }

        //获取标签列表
        List<Tag> tagList = tagEntityService.list();
        HashMap<Long, String> tagMap = new HashMap<>();
        for (Tag tag : tagList) {
            tagMap.put(tag.getId(), tag.getName());
        }

        //文件下载缓存文件夹
        String workDir = Constants.File.FILE_DOWNLOAD_TMP_FOLDER.getPath() + File.separator + IdUtil.simpleUUID();

        /*
            线程池流程：
            1.线程总量 < corePoolSize，即使线程池中的线程都处于空闲状态，也要创建新的线程来处理被添加的任务。
            2.线程总量 = corePoolSize，但 阻塞队列 workQueue 未满，那么任务被放入缓冲队列。
            3.线程总量 > corePoolSize，且 阻塞队列 workQueue 已满，且 线程池中的 线程总量 < maximumPoolSize，则 新建线程 来处理被添加的任务。
            4.线程总量 > corePoolSize，且 缓冲队列 workQueue 已满，且 线程池中的 线程总量 = maximumPoolSize，则 根据 handler 执行对应的拒绝策略。
            5.线程总量 > corePoolSize，且 某线程空闲时间 > keepAliveTime，线程将被终止。因此线程池可以动态调整池中的线程数。
         */
        //核心线程数
        int corePoolSize = 2;
        //最大线程数，最多允许几个线程并发
        int maximumPoolSize = 4;
        //当非核心线程无任务时，几秒后结束该线程
        int keepAliveTime = 3;
        ExecutorService threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                //结束线程时间单位
                TimeUnit.SECONDS,
                //阻塞队列，限制等候线程数
                new LinkedBlockingDeque<>(200),
                //创建线程工厂
                Executors.defaultThreadFactory(),
                //设置线程池拒绝策略：DiscardOldestPolicy
                //当有任务添加到线程池被拒绝时，线程池会丢弃阻塞队列中末尾的等待最久的任务，然后将被拒绝的任务添加到末尾。
                //队列满时不会抛出异常，尝试去和最早的线程竞争，竞争失败后才抛弃
                new ThreadPoolExecutor.DiscardOldestPolicy());

        //保存所有题目的下载任务
        List<FutureTask<Void>> futureTasks = new ArrayList<>();

        for (Long pid : pidList) {

            //添加下载任务
            futureTasks.add(new FutureTask<>(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    //根据题目ID拼接测试用例保存文件夹地址
                    String testcaseWorkDir = Constants.File.TESTCASE_BASE_FOLDER.getPath() + File.separator + "problem_" + pid;
                    File file = new File(testcaseWorkDir);

                    //获取题目的测试用例
                    List<HashMap<String, Object>> problemCases = new LinkedList<>();
                    if (!file.exists() || file.listFiles() == null) {
                        //本地为空时尝试去数据库查找
                        QueryWrapper<ProblemCase> problemCaseQueryWrapper = new QueryWrapper<>();
                        problemCaseQueryWrapper.eq("pid", pid);
                        List<ProblemCase> problemCaseList = problemCaseEntityService.list(problemCaseQueryWrapper);
                        FileUtil.mkdir(testcaseWorkDir);
                        //写入本地
                        for (int i = 0; i < problemCaseList.size(); i++) {
                            //文件名前缀
                            String filePreName = testcaseWorkDir + File.separator + (i + 1);
                            //写入输入用例
                            String inputName = filePreName + ".in";
                            FileWriter infileWriter = new FileWriter(inputName);
                            infileWriter.write(problemCaseList.get(i).getInput());
                            //写入输出用例
                            String outputName = filePreName + ".out";
                            FileWriter outfileWriter = new FileWriter(outputName);
                            outfileWriter.write(problemCaseList.get(i).getOutput());
                            //设置题目测试用例信息
                            ProblemCase problemCase = problemCaseList.get(i).setPid(null)
                                    .setInput(inputName)
                                    .setOutput(outputName)
                                    .setStatus(null)
                                    .setId(null)
                                    .setCreateTime(null)
                                    .setModifiedTime(null);
                            HashMap<String, Object> problemCaseMap = new HashMap<>();
                            BeanUtil.beanToMap(problemCase, problemCaseMap, false, true);
                            problemCases.add(problemCaseMap);
                        }
                        FileUtil.copy(testcaseWorkDir, workDir, true);
                    } else {
                        //从本地文件中获取
                        String infoPath = testcaseWorkDir + File.separator + "info";
                        if (FileUtil.exist(infoPath)) {
                            //读取json格式的测试用例
                            FileReader reader = new FileReader(infoPath);
                            JSONObject jsonObject = JSONUtil.parseObj(reader.readString());
                            JSONArray testCases = jsonObject.getJSONArray("testCases");
                            for (int i = 0; i < testCases.size(); i++) {
                                JSONObject jsonObject1 = testCases.get(i, JSONObject.class);
                                //获取测试用例信息
                                HashMap<String, Object> problemCaseMap = new HashMap<>();
                                problemCaseMap.put("input", jsonObject1.getStr("inputName"));
                                problemCaseMap.put("output", jsonObject1.getStr("outputName"));
                                Integer score = jsonObject1.getInt("score");
                                Integer groupNum = jsonObject1.getInt("groupNum");
                                if (score != null && score > 0) {
                                    problemCaseMap.put("score", score);
                                }
                                if (groupNum != null) {
                                    problemCaseMap.put("groupNum", groupNum);
                                }
                                problemCases.add(problemCaseMap);
                            }
                        }
                        FileUtil.copy(testcaseWorkDir, workDir, true);
                    }

                    //导出题目文件
                    ImportProblemVO importProblemVo = problemEntityService.buildExportProblem(pid, problemCases, languageMap, tagMap);
                    String content = JSONUtil.toJsonStr(importProblemVo);
                    FileWriter fileWriter = new FileWriter(workDir + File.separator + "problem_" + pid + ".json");
                    fileWriter.write(content);
                    return null;
                }
            }));

        }
        //将保存的下载任务提交到线程池进行执行
        for (FutureTask<Void> futureTask : futureTasks) {
            threadPool.submit(futureTask);
        }
        //所有任务执行完成且等待队列中无任务时 关闭线程池
        if (!threadPool.isShutdown()) {
            threadPool.shutdown();
        }
        //阻塞主线程, 直至线程池关闭
        try {
            threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            log.error("线程池异常--------------->", e);
        }

        String fileName = "problem_export_" + System.currentTimeMillis() + ".zip";
        //将对应文件夹的文件压缩成zip
        ZipUtil.zip(workDir, Constants.File.FILE_DOWNLOAD_TMP_FOLDER.getPath() + File.separator + fileName);
        //将zip变成io流返回给前端
        FileReader fileReader = new FileReader(Constants.File.FILE_DOWNLOAD_TMP_FOLDER.getPath() + File.separator + fileName);
        //放到缓冲流里面
        BufferedInputStream bins = new BufferedInputStream(fileReader.getInputStream());
        //获取文件输出IO流
        OutputStream outs = null;
        BufferedOutputStream bouts = null;
        try {
            outs = response.getOutputStream();
            bouts = new BufferedOutputStream(outs);
            //设置响应类型为前端提供下载数据
            response.setContentType("application/x-download");
            response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
            int bytesRead = 0;
            byte[] buffer = new byte[1024 * 10];
            //开始向网络传输文件流
            while ((bytesRead = bins.read(buffer, 0, 1024 * 10)) != -1) {
                bouts.write(buffer, 0, bytesRead);
            }
            bouts.flush();
        } catch (IOException e) {
            log.error("导出题目数据的压缩文件异常------------>", e);
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            Map<String, Object> map = new HashMap<>();
            map.put("status", ResultStatus.SYSTEM_ERROR);
            map.put("msg", "导出题目数据失败，请重新尝试！");
            map.put("data", null);
            try {
                response.getWriter().println(JSONUtil.toJsonStr(map));
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        } finally {
            try {
                bins.close();
                if (outs != null) {
                    outs.close();
                }
                if (bouts != null) {
                    bouts.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 清空临时文件
            FileUtil.del(workDir);
            FileUtil.del(Constants.File.FILE_DOWNLOAD_TMP_FOLDER.getPath() + File.separator + fileName);
        }
    }

}
